/*
 * Code for blog.techhead.biz
 * Distributed under BSD-style license
 */

package biz.techhead.funcy;

import java.lang.reflect.Field;
import java.util.Arrays;

/**
 * A Tuple is an immutable list of typed objects (which may be of
 * different types). It is thread safe and implements hashCode and equals.
 *
 * <p>Dynamic, array-like access is provided through the use of
 * {@code get} and {@code toArray}, but the real strength of Tuples
 * comes through the use of its statically typed fields.
 * 
 * <p><code>
 * import static biz.techhead.funcy.Tuples.*;
 * // example
 * T2<Integer,String> t = T(1, "two");
 * int one = t._1;
 * String two = t._2;
 * </code>
 *
 * <p>The manner in which this Tuple class was implemented was inspired by the
 * blog post: http://gleichmann.wordpress.com/2008/01/15/building-your-own-literals-in-java-tuples-and-maps/
 *
 * Before finding the article, I had intended to provide something similar
 * to javatuple.com
 * 
 * @author Jonathan Hawkes <jhawkes at techhead.biz>
 */
public abstract class Tuple {

    /**
     * The number of elements the Tuple contains.
     * This will not change.
     */
    public final int length;

    /**
     * New Tuple.
     * @param length The immutable length of the Tuple.  Must have a public
     *          field named '_n' for every integer 1 to length inclusive.
     * @param hashCode The hashCode provided must hold true for
     *          Tuples.hashCode( ((Tuple)t).toArray() ) == t.hashCode()
     */
    Tuple(int length, int hashCode) {
        this.length = length;
        this.hashCode = hashCode;
    }
    
    /**
     * Fetch a value out of the Tuple dynamically.  The index starts at 0.
     *
     * <code>
     * Tuples.T1<Integer> theLonliestNumber = Tuples.T(1);
     * assert theLonliestNumber.get(0).equals(theLonliestNumber._1);
     * </code>
     * 
     * @throws IndexOutOfBoundsException if index >= Tuple.length
     */
    public Object get(int i) {
        return getValues()[i];
    }

    /**
     * Loads or retrieves an array view of the Tuple.
     * This method is thread safe.
     */
    private Object[] getValues() {
        Object[] retValues = values;
        // double-checked locking
        if (retValues == null) {
            synchronized (this) {
                retValues = values;
                if (retValues == null) {
                    retValues = values = initValues();
                }
            }
        }
        return retValues;
    }

    /**
     * Extracts the values of all public fields named '_n' where 'n'
     * is the 1-based index into the Tuple.
     */
    private Object[] initValues() {
        int found = 0;
        Object[] foundValues = new Object[length];
        for (Field field : getClass().getFields()) {
            try {
                String fieldName = field.getName();
                if ( fieldName.startsWith("_") ) {
                    int i = Integer.parseInt(fieldName.substring(1)) - 1;
                    if (i < length) {
                        found++;
                        foundValues[i] = field.get(this);
                    }
                }
            } catch (NumberFormatException nfe) {
                //continue
            } catch (IllegalAccessException iae) {
                // Field is public
                assert false;
            }
        }
        assert (found == length) :
            "Violation of Tuple contract - length.";
        assert (hashCode(foundValues) == hashCode) :
            "Violation of Tuple contract - hashCode.";
        return foundValues;
    }

    /**
     * Returns an array representation of the Tuple.
     * Changing the contents of the array will not alter the immutable Tuple.
     */
    public Object[] toArray() {
        return getValues().clone();
    }

    @Override
    public int hashCode() {
        return hashCode;
    }

    @Override
    /**
     * A Tuple is equal to another Tuple if it has the same length
     * and all its values are also equal.
     */
    public boolean equals(Object o) {
        if ( (o instanceof Tuple) &&
                (((Tuple)o).length == length) &&
                (((Tuple)o).hashCode == hashCode) ) {
            return Arrays.equals( ((Tuple)o).getValues(),
                                             getValues() );
        }
        return false;
    }

    @Override
    public String toString() {
        Object[] v = getValues();
        if (v.length == 0) {
            return "()";
        } else {
            StringBuilder sb = new StringBuilder();
            sb.append("(").append(v[0]);
            for (int i=1; i<v.length; i++) {
                sb.append(", ").append(v[i]);
            }
            return sb.append(")").toString();
        }
    }

    private static int hashCode(Object o) {
        return (o == null) ? 0 : o.hashCode();
    }

    protected static int hashCode(int hashCode, Object o) {
        return (31 * hashCode) + hashCode(o);
    }

    /**
     * assert Tuple.hashCode(x, y, z) == Tuples.T(x, y, z).hashCode();
     */
    public static int hashCode(Object... os) {
        if (os == null) return 0;
        int result = 1;
        for (int i=os.length; i>-1; i--) {
            result = hashCode(result, os[i]);
        }
        return result;
    }

    /**
     * Final hashCode is computed and cached
     * upon Tuple creation.
     */
    private final int hashCode;

    /**
     * This is a lazy-loaded array view
     * of the Tuple.
     */
    private volatile Object[] values;
}
